// WebModel 是对 BaseModel的扩展
// 在前端处理 fields_get, web_read, web_search_read, onchange 三个函数的扩展
// 1. 参数 fields: Dict. 支持嵌套读取子模型
// 11.  对于 fields_get 函数. fields 中包含 view 带过来的额外的  domian 和 selection
// 12.  对于 web_read 函数. fields 中仅仅是 嵌套要读取的 结构
// 2. fields_get 返回结果中包含子模型的结果
// 4. web_read, web_search_read 返回结果进行标准化处理
// 41. m2o字段标准化为 {id, display_name}
// 42. m2m字段标准化为 [{id, display_name, field2, field3, ...}]
// 43. o2m字段标准化为 [{id, field1, field2, field3, ...}]
// 5. onchange 函数需要:
// 50. 涉及到 editmodel. 考虑是否集成 editmodel的一些处理逻辑
// 51. 返回结果进行标准化处理

import { BaseModel } from './basemodel.js'

class WebModelFirst extends BaseModel {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
    const { metadata = {} } = payload
    this._metadata = metadata
  }

  get metadata() {
    return this._metadata
  }

  set metadata(val) {
    this._metadata = { ...val }
  }

  get _rec_names_search() {
    return ['name']
  }

  async _js_web2_name_search(kwargs = {}) {
    // const { name, args, operator, fields, limit, offset } = kwargs
    const {
      fields = {},
      args = [],
      name,
      limit = 8,
      offset = 0,
      order
    } = kwargs

    const ns = this._rec_names_search

    const mydomain = ns.reduce((acc, item) => {
      const me = [item, 'ilike', name]
      if (acc.length === 0) {
        acc.push(me)
      } else {
        acc = ['|', ...acc, me]
      }
      return acc
    }, [])

    const domain = [...mydomain, ...args]

    const fields2 = Object.keys(fields).length ? fields : { display_name: {} }
    if (!('display_name' in fields)) {
      fields2.display_name = {}
    }

    const res = await this._js_web2_search_read(domain, fields2, {
      offset,
      limit,
      order
    })
    const { length, records } = res
    return { length, offset, limit, records }
  }

  async fields_get(allfields, kwargs = {}) {
    const fs = await super.fields_get(allfields, kwargs)

    return Object.keys(fs).reduce((acc, fld) => {
      // x2m 字段 补上 metadata 属性. 后续其他用的时候, 无需做非空处理
      const x2m = ['many2many', 'one2many'].includes(fs[fld].type)
        ? { metadata: {} }
        : {}

      acc[fld] = { ...fs[fld], ...x2m }
      return acc
    }, {})
  }

  // 参数 fields 格式
  // dict  可递归处理relation字段.
  // 包括:
  // 1. _js_web2_fields_get 的 allfields
  // 2. _js_web2_search_read 的 specification
  //

  // 参数 callback
  // 函数. 分多次返回 包含嵌套子模型的 结果
  // 包括:
  // 1. _js_web2_fields_get 的 kwargs.callback
  // 2. _js_web2_search_read  的 kwargs.callback

  async _js_web2_fields_get(allfields, kwargs = {}) {
    // console.log(allfields)
    const { callback, context } = kwargs

    const cb = () => {
      if (callback) {
        callback(this.metadata)
      }
    }

    const fields_list = Object.keys(allfields)

    // call 1st. main model
    const metadata0 = await this.fields_get(fields_list, { context })

    const metadata = Object.keys(metadata0).reduce((acc, fld) => {
      acc[fld] = { ...metadata0[fld], ...(allfields[fld] || {}) }
      return acc
    }, {})

    this.metadata = { ...metadata }
    cb()

    // call 2nd. relation model
    for (const fld of fields_list) {
      const meta = metadata[fld]
      if (!meta) {
        continue
      }

      if (!['many2one', 'many2many', 'one2many'].includes(meta.type)) {
        continue
      }

      const meta2 = allfields[fld] || {}
      const child_fields = meta2.fields
      if (!child_fields) {
        continue
      }

      const RM = this.env.model(meta.relation)
      const rel_metadata = await RM._js_web2_fields_get(child_fields, {
        callback: res => {
          this.metadata = {
            ...this.metadata,
            [fld]: { ...meta, metadata: res }
          }
          cb()
        },
        context
      })

      this.metadata = {
        ...this.metadata,
        [fld]: { ...meta, metadata: rel_metadata }
      }
      cb()
    }

    return this.metadata
  }

  _js_fields_spec_for_onchange(metadata) {
    const spec = Object.keys(metadata)
      .filter(fld => !(metadata[fld] || {}).disable_field_onchange)
      .reduce((acc, fld) => {
        acc[fld] = {}
        const meta = metadata[fld]
        if (['many2one', 'many2many', 'one2many'].includes(meta.type)) {
          const o2m_metadata = meta.metadata || {}
          const child_fields = this._js_fields_spec_for_onchange(o2m_metadata)
          if (!('display_name' in child_fields)) {
            child_fields.display_name = {}
          }
          acc[fld] = { fields: child_fields }
        }

        return acc
      }, {})

    return this._js_format_fields_spec(spec, { metadata })
  }

  _js_format_fields_spec(spec, { metadata }) {
    // odoo 17 todo
    // fields_spec
    // 1. 子表的字段
    // 2. 子表的排序
    // 3. ?其他?

    const spec2 = spec || {}
    return Object.keys(spec2).reduce((acc, fld) => {
      // 参数中的非法字段. 直接 过滤掉, 否则 接口中出错
      if (fld in metadata) {
        acc[fld] = {}
        const meta = metadata[fld] || {}

        if (['many2one', 'many2many', 'one2many'].includes(meta.type)) {
          const o2m_metadata = { ...(meta.metadata || {}) }

          if (!Object.keys(o2m_metadata).length) {
            o2m_metadata.display_name = { type: 'char' }
          }

          const rel_field = (spec || {})[fld] || {}
          const o2m_spec = rel_field.fields || { display_name: {} }
          const child_fields = this._js_format_fields_spec(o2m_spec, {
            metadata: o2m_metadata
          })
          acc[fld] = { fields: child_fields }
        }
      } else {
        acc[fld] = {}
      }

      return acc
    }, {})
  }

  async _js_web2_search_read(domain, specification, kwargs = {}) {
    const metadata = this.metadata
    const spec = this._js_format_fields_spec(specification, { metadata })

    const res = await this.web_search_read(domain, spec, kwargs)
    const { length, records } = res
    const records2 = this._js_formatList(records, specification, { metadata })

    return { length, records: records2 }
  }

  async _js_web2_read(res_ids, specification, kwargs = {}) {
    const metadata = this.metadata
    const spec = this._js_format_fields_spec(specification, { metadata })
    const records = await this.web_read(res_ids, spec, kwargs)
    return this._js_formatList(records, specification, { metadata })
  }

  async _js_web2_onchange(ids, values, field_name, kwargs = {}) {
    const metadata = this.metadata
    const fields_spec = this._js_fields_spec_for_onchange(metadata)
    const args = [ids, values, field_name, fields_spec, kwargs]
    const res = await this.onchange(...args)
    // odoo17 res maybe = {}
    const { value = {}, ...res2 } = res
    const value2 = this._js_formatDict(value, fields_spec, {
      metadata,
      for_onchange: true
    })
    return { value: value2, ...res2 }
  }

  async _js_web2_save(res_id, vals, kwargs = {}) {
    const metadata = this.metadata
    const specification = this._js_fields_spec_for_onchange(metadata)
    const records = await this.web_save(res_id, vals, specification, kwargs)
    const records2 = this._js_formatList(records, specification, { metadata })
    const one = records2.length ? records2[0] : {}
    return one
  }

  _js_formatList(records, specification, kwargs = {}) {
    return records.map(one => {
      return this._js_formatDict(one, specification, kwargs)
    })
  }

  _js_formatDict(one, specification = {}, { metadata, for_onchange }) {
    // 返回值 已经包含嵌套数据
    // 1. 空值 false 转 null
    // 2. specification 中 未指定的 relation 字段. 做标准化
    // 3. 嵌套对象中的数据 递归处理
    // 4. m2o, read/onchange
    // 41. false.
    // 42. [id, name]
    // 43. {id, display_name, ...}
    // 5. m2m/m2m, read
    // 51. []
    // 52. [id,id]
    // 53. [{id, ...}]
    // 6. m2m, onchange
    // 61. []
    // 62. [?]
    // 63. [?]
    // 7. o2m, onchange
    // 71. []
    // 72. [?]
    // 73. [?]

    // console.log('_js_formatDict', one)

    const fmt = (fld, val) => {
      // console.log('_js_formatDict 1', fld, specification, val)
      const meta = metadata[fld] || {}
      const spec1 = specification[fld] || {}
      // console.log('_format_dict 1', fld, meta.type, spec1, val)
      // m2o, false, [id, 'name']
      // x2m, [], [ids,1,2,3,4]
      // selection, false, 'string'
      // date, false, 'yyyy-mm-dd'
      // datetime, false, 'yyyymmdd hh:mm:ss.sssssss'
      //

      if (meta.type === 'boolean') {
        return val
      } else if (meta.type === 'integer') {
        return val !== false ? val : 0
      } else if (meta.type === 'float') {
        return val !== false ? val : 0.0
      } else if (meta.type === 'monetary') {
        return val !== false ? val : 0.0
      } else if (meta.type === 'many2one') {
        if (!val) {
          return { id: null }
        } else {
          if (Array.isArray(val)) {
            // console.log('tocheck, m2o, return [id,name]:', fld, val)
            return { id: val[0], display_name: val[1] }
          } else {
            // console.log('tocheck, m2o, return {id }:', fld, val)
            return this._js_formatDict(val, spec1.fields, {
              metadata: meta.metadata || {}
            })
          }
        }
      } else if (['many2many', 'one2many'].includes(meta.type)) {
        if (for_onchange) {
          if (meta.type === 'many2many') {
            console.log('tocheck, m2m, for_onchange:', fld, val)
            return this._js_formatDict_onchange_many2many(val, meta)
          } else {
            console.log('tocheck, O2M, for_onchange:', fld, val)
            return this._js_formatDict_onchange_one2many(val, meta)
          }
        } else {
          return val.map(item => {
            if (typeof item === 'number') {
              console.log('tocheck, m2m/O2M, return number:', fld, val)
              return { id: item }
            } else if (typeof item === 'object') {
              // console.log('tocheck, m2m/O2M, return object:', fld, val)
              return this._js_formatDict(item, spec1.fields, {
                metadata: meta.metadata || {}
              })
            } else {
              console.log('tocheck, m2m/O2M, return error:', fld, val)
              return item
            }
          })
        }
      } else if (['reference', 'many2one_reference'].includes(meta.type)) {
        // todo. reference, many2one_reference 字段 需要时再处理
        return val || null
      } else {
        return val || null
      }
    }

    return Object.keys(one).reduce((acc, fld) => {
      acc[fld] = fmt(fld, one[fld])
      return acc
    }, {})
  }

  _js_formatDict_onchange_many2many(tuples, meta) {
    // todo  m2m
    // onchange_new 返回 [[5,0,0]]
    // 可能返回 [[6,false, [ids]]]
    //   可能返回? [[4, id, false]]
    // 后续需要取 name_get
    // console.log('todo m2m, onchange1 return,', fld, tuples)

    const records2 = tuples.reduce((acc, tup) => {
      const [op, rid, val] = tup
      if (op === 5) {
        acc = {}
      } else if (op === 6) {
        acc = val.reduce((acc2, cur) => {
          acc2[cur] = { id: cur }
          return acc2
        }, {})
      } else if (op === 4) {
        acc[rid] = { id: rid }
      } else {
        console.log('todo : _js_formatDict_onchange_many2many:', tuples)
      }

      return acc
    }, {})

    return Object.values(records2)
  }

  _js_formatDict_onchange_one2many(val, meta) {
    // 自定义 扩展 模块 stock2.quant.picking.wizard 返回 line_ids,
    console.log('todo o2m, onchange1 return,', val, meta)
    //
    // 返回值:
    // [[5,0,0],   // 无需处理. 后续 做 merge
    //  [4,id,false], // 需要 read? 实际上 , 第一次读 主model时, 一定读取过了
    //  [2,id,false], //
    //  [1,id,{childval}], // 对childval 做标准化?
    //  [0,id,{}],    //
    // ]
    //

    const done = val.map(item => {
      const op = item[0]
      if (op === 5) {
        return [op, false, false]
      } else if (op === 4) {
        const rid = item[1]
        return [op, rid, { id: rid }]
      } else if (op === 2) {
        const rid = item[1]
        return [op, rid, false]
      } else if (op === 1 || op === 0) {
        const rid = item[1]
        const chvals = item[2]

        const chvals2 = this._js_formatDict(
          chvals,
          {},
          { metadata: meta.metadata, for_onchange: 1 }
        )
        return [op, rid, chvals2]
      } else {
        const item2 = item
        return item
      }
    })

    return done
    // 2023-10-26 旧代码.
    // const Rel = env.one2many(meta)
    // const val2 = Rel._merge_values(val)
    // return val2
  }
}

class WebModel17 extends WebModelFirst {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }
}

class WebModel16 extends WebModel17 {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }

  async _js16_web2_search_read(domain, specification, kwargs = {}) {
    const { callback, ...kwargs2 } = kwargs

    const fields = Object.keys(specification)
    const res = await this.web_search_read(domain, fields, kwargs2)
    const { length, records } = res

    const cb = data => {
      if (callback) {
        callback(data)
      }
    }

    const { context = {} } = kwargs2

    const done = this._js16_formatList(records, this.metadata)

    cb({ length, records: done })
    const records2 = await this._js16_web2_read_next(done, specification, {
      callback: cb,
      context
    })

    return { length, records: records2 }
  }

  async _js16_web2_read(res_ids, specification, kwargs = {}) {
    const { callback, ...kwargs2 } = kwargs

    const fields = Object.keys(specification || this.metadata)
    const records = await this.read(res_ids, fields, kwargs2)

    const done = this._js16_formatList(records, this.metadata)

    const cb = data => {
      if (callback) {
        callback(data)
      }
    }

    const { context = {} } = kwargs2

    cb(done)
    return this._js16_web2_read_next(done, specification, {
      callback: res => cb(res),
      context
    })
  }

  async _js16_web2_read_next(records, specification, kwargs = {}) {
    const { callback, context = {} } = kwargs
    let done_dict = records.reduce((acc, cur) => {
      acc[cur.id] = cur
      return acc
    }, {})

    const merge_to_done_dict = (fld, list) => {
      const meta = this.metadata[fld] || {}
      const dict2 = list.reduce((acc, one) => {
        acc[one.id] = one
        return acc
      }, {})

      done_dict = Object.keys(done_dict).reduce((acc, oneid) => {
        const one = done_dict[oneid]
        const one2 = { ...one }

        if (['many2one'].includes(meta.type)) {
          if (one[fld].id) {
            one2[fld] = dict2[one[fld].id]
          }
        } else {
          one2[fld] = one[fld].map(item => dict2[item.id])
        }

        acc[oneid] = one2

        return acc
      }, {})

      const new_records = records.map(item => done_dict[item.id])
      callback(new_records)
    }

    const filter_fields = fld => {
      const item = specification[fld] || {}

      if (!item) {
        return false
      } else if (typeof item === 'object') {
        if (item.fields) {
          return true
        } else {
          const meta = this.metadata[fld] || {}
          if (['many2many', 'one2many'].includes(meta.type)) {
            // m2m tags  没有 fields, 但是需要 递归查询

            return true
          } else {
            return false
          }
        }
      } else {
        const meta = this.metadata[item] || {}
        if (['many2many'].includes(meta.type)) {
          // m2m tags  没有 fields, 但是需要 递归查询
          return true
        } else {
          return false
        }
      }
    }

    const todo = Object.keys(specification)
      .filter(filter_fields)
      .reduce((acc, fld) => {
        acc[fld] = { ids: [], fields: specification[fld].fields }
        return acc
      }, {})

    records.forEach(one => {
      Object.keys(todo).forEach(fld => {
        const val = one[fld]
        const meta = this.metadata[fld] || {}

        if (['many2many', 'one2many'].includes(meta.type)) {
          const rel_new_ids = val.map(item => item.id)
          const rel_old_ids = todo[fld].ids
          const rel_ids1 = [...rel_old_ids, ...rel_new_ids]
          const rel_ids = [...new Set(rel_ids1)]
          todo[fld].ids = rel_ids
        } else if (['many2one'].includes(meta.type)) {
          const rel_new_ids = val.id ? [val.id] : []
          const rel_old_ids = todo[fld].ids
          const rel_ids1 = [...rel_old_ids, ...rel_new_ids]
          const rel_ids = [...new Set(rel_ids1)]
          todo[fld].ids = rel_ids
        }
      })
    })

    for (const fld of Object.keys(todo)) {
      const meta = this.metadata[fld] || {}

      const todoids = todo[fld].ids

      if (todoids.length === 0) {
        // do nothing
      } else if (meta.metadata && Object.keys(meta.metadata).length) {
        const RM = this.env.model(meta.relation, {
          metadata: meta.metadata
        })

        const child_fields = todo[fld].fields || { display_name: {} }
        const rel_data = await RM._js16_web2_read(todoids, child_fields, {
          callback: res => {
            merge_to_done_dict(fld, res)
          },
          context
        })

        merge_to_done_dict(fld, rel_data)
      } else {
        const RM = this.env.model(meta.relation)
        const rel_data0 = await RM.name_get(todoids, { context })
        const rel_data = rel_data0.map(item => ({
          id: item[0],
          display_name: item[1]
        }))
        merge_to_done_dict(fld, rel_data)
      }
    }

    const new_records = records.map(item => done_dict[item.id])

    return new_records
  }

  _js16_formatList(records, metadata) {
    return records.map(one => {
      return this._js16_formatDict(one, metadata)
    })
  }

  _js16_formatDict(one, metadata, for_onchange) {
    // 返回值 未包含嵌套数据
    // 1. 空值 false 转 null
    // 2. relation 字段. 做标准化
    // 4. m2o, read/onchange
    // 41. false.
    // 42. [id, name]
    // 5. m2m/m2m, read
    // 51. []
    // 52. [id,id]
    // 6. m2m, onchange
    // 61. []
    // 62. [?]
    // 7. o2m, onchange
    // 71. []
    // 72. [?]

    // console.log('_js_formatDict', one)

    const fmt = (fld, val) => {
      const meta = metadata[fld] || {}
      // console.log('_format_dict 1', fld, meta)
      // m2o, false, [id, 'name']
      // x2m, [], [ids,1,2,3,4]
      // selection, false, 'string'
      // date, false, 'yyyy-mm-dd'
      // datetime, false, 'yyyymmdd hh:mm:ss.sssssss'
      //

      if (meta.type === 'boolean') {
        return val
      } else if (meta.type === 'integer') {
        return val !== false ? val : 0
      } else if (meta.type === 'float') {
        return val !== false ? val : 0.0
      } else if (meta.type === 'monetary') {
        return val !== false ? val : 0.0
      } else if (meta.type === 'many2one') {
        if (!val) {
          return { id: null }
        } else {
          // console.log('tocheck, m2o, return [id,name]:', fld, val)
          return { id: val[0], display_name: val[1] }
        }
      } else if (['many2many', 'one2many'].includes(meta.type)) {
        if (for_onchange) {
          if (meta.type === 'many2many') {
            console.log('tocheck, m2m, for_onchange:', fld, val)
            return this._js_formatDict_onchange_many2many(val, meta)
          } else {
            console.log('tocheck, O2M, for_onchange:', fld, val)
            return this._js_formatDict_onchange_one2many(val, meta)
          }
        } else {
          return val.map(item => ({ id: item }))
        }
      } else {
        return val || null
      }
    }

    return Object.keys(one).reduce((acc, fld) => {
      acc[fld] = fmt(fld, one[fld])
      return acc
    }, {})
  }

  _js16_get_field_onchange(metadata) {
    // call by onchange
    // todo 不取所有字段. 应该取当前页面
    // groups 有权限的 字段
    // 实际上 影响不大
    // 参考测试: account.move 的 partner_shipping_id 字段
    return Object.keys(metadata)
      .filter(fld => !metadata[fld].disable_field_onchange)
      .reduce((acc, fld) => {
        acc[fld] = '1'
        const meta = metadata[fld]
        if (meta.type === 'one2many') {
          // console.log(meta.metadata)
          const o2m_metadata = meta.metadata
          const o2m_fields = Object.keys(o2m_metadata).reduce((acc2, fld2) => {
            acc2[`${fld}.${fld2}`] = '1'
            return acc2
          }, {})
          acc = { ...acc, ...o2m_fields }
        }
        return acc
      }, {})
  }

  async _js16_web2_onchange(ids, values, field_name, kwargs = {}) {
    const field_onchange = this._js16_get_field_onchange(this.metadata)
    const args = [ids, values, field_name, field_onchange, kwargs]
    const res = await this.onchange(...args)
    const { value, ...res2 } = res
    const value2 = await this._js16_web2_onchange_after(value, kwargs)
    return { ...res2, value: value2 }
  }

  async _js16_web2_onchange_after(value, kwargs = {}) {
    const value2 = this._js16_formatDict(value, this.metadata, true)
    // console.log('onchange1, ok', field_name, value2)
    //
    // 返回结果中 有 m2m, o2m 字段 需要补充 信息
    //

    const { context } = kwargs

    const value3 = {}

    for (const fld of Object.keys(value2)) {
      const meta = this.metadata[fld] || {}

      if (['one2many'].includes(meta.type)) {
        // 自定义 扩展 模块 stock2.quant.picking.wizard 返回 line_ids,
        console.log('todo, onchange1 return o2m', fld, value2[fld])
        value3[fld] = value2[fld]
      } else if (['many2many'].includes(meta.type)) {
        // sale.order 模型 的 line 新增时, 有默认税字段
        // 自定义 扩展 模块 stock2.quant.picking.wizard 返回 quant_ids,
        // console.log('onchange1, next m2m', fld, meta, value2[fld])
        const rel_val = value2[fld]
        const rel_ids = rel_val.map(item => item.id)
        if (rel_ids.length) {
          const RM = this.env.model(meta.relation, {
            metadata: meta.metadata
          })

          const child_spec = this._js_fields_spec_for_onchange(meta.metadata)

          const child_spec2 =
            Object.keys(child_spec).length > 0
              ? child_spec
              : { display_name: {} }

          const rel_data = await RM._js16_web2_read(rel_ids, child_spec2, {
            context
          })

          value3[fld] = rel_data
        } else {
          value3[fld] = rel_val
        }
      } else {
        value3[fld] = value2[fld]
      }
    }

    return value3
  }

  async _js16_web2_save(res_id, vals, kwargs = {}) {
    // odoo16 返回 是 ID
    if (res_id) {
      await this.write(res_id, vals, kwargs)
      return { id: res_id }
    } else {
      const rid = await this.create(vals, kwargs)
      return { id: rid }
    }
    // todo call web_read
    // const one2 = this._js16_formatDict(one, specification, true)
    // return one2
  }

  async _js_web2_fields_get(allfields, kwargs = {}) {
    return super._js_web2_fields_get(allfields, kwargs)
  }

  async _js_web2_search_read(domain, specification, kwargs = {}) {
    const version = this.env.server_version

    if (version > 16) {
      return super._js_web2_search_read(domain, specification, kwargs)
    } else {
      return this._js16_web2_search_read(domain, specification, kwargs)
    }
  }

  async _js_web2_read(res_ids, specification, kwargs = {}) {
    const version = this.env.server_version
    if (version > 16) {
      return super._js_web2_read(res_ids, specification, kwargs)
    } else {
      return this._js16_web2_read(res_ids, specification, kwargs)
    }
  }

  async _js_web2_onchange(ids, values, field_name, kwargs = {}) {
    const version = this.env.server_version
    if (version > 16) {
      return super._js_web2_onchange(ids, values, field_name, kwargs)
    } else {
      return this._js16_web2_onchange(ids, values, field_name, kwargs)
    }
  }

  async _js_web2_save(res_id, vals, kwargs = {}) {
    const version = this.env.server_version
    if (version > 16) {
      return super._js_web2_save(res_id, vals, kwargs)
    } else {
      return this._js16_web2_save(res_id, vals, kwargs)
    }
  }
}

class WebModel13 extends WebModel16 {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }

  async default_get(fields_list, kwargs = {}) {
    // const { context = {} } = kwargs
    const method = 'default_get'
    const args = [fields_list]
    return this.call_kw(method, args, kwargs)
  }

  async _js13_default_get_onchange(values, kwargs = {}) {
    const fields_list = Object.keys(this.metadata)
    const default_values = await this.default_get(fields_list, kwargs)

    // # TBD: default_get 里面 可能有 m2o o2m 需要处理
    // default_get, m2o 返回值 是 id, 需要 补充上 display_name
    const default_values2 = {}
    for (const col of Object.keys(default_values)) {
      const meta = this.metadata[col]
      // console.log('default get 2', col, meta, default_values)
      // TBD, res.config.settings 出现了  meta 为空的 情况

      const ref_val = default_values[col]

      if (meta && meta.relation && meta.type === 'many2one') {
        if (ref_val) {
          const ref_ids = Array.isArray(ref_val) ? ref_val : [ref_val]
          const ref_records = await this.env
            .model(meta.relation)
            .name_get(ref_ids, kwargs)

          const ref_rec = ref_records[0]

          default_values2[col] = [...ref_rec]
        } else {
          default_values2[col] = ref_val
        }
      } else {
        default_values2[col] = ref_val
      }
    }

    const _get_default = col => {
      const meta = this.metadata[col]
      if (['many2many'].includes(meta.type)) return [[6, false, []]]
      else if (['one2many'].includes(meta.type)) return []
      else if (['float', 'integer', 'monetary'].includes(meta.type)) return 0
      else if (['text', 'html'].includes(meta.type)) return ''
      return false
    }

    const values0 = fields_list.reduce((acc, cur) => {
      acc[cur] = _get_default(cur)
      return acc
    }, {})

    const values2 = { ...values0, ...values, ...default_values }

    // 直接 call super 无法 merge default_get + onchange 的 结果
    // return super._js_web2_onchange([], values2, fields_list, kwargs)
    // 因此 只能 重新 写一遍

    const field_onchange = this._js16_get_field_onchange(this.metadata)
    const args = [[], values2, fields_list, field_onchange, kwargs]
    const res = await this.onchange(...args)
    const { value, ...res2 } = res
    const value1 = { ...default_values2, ...value }
    const value2 = await this._js16_web2_onchange_after(value1, kwargs)
    return { ...res2, value: value2 }
  }

  async _js13_web2_onchange(ids, values, field_name, kwargs = {}) {
    function is_call_default(field_name) {
      if (!field_name) {
        return true
      } else if (Array.isArray(field_name) && !field_name.length) {
        return true
      } else {
        return false
      }
    }
    if (is_call_default(field_name)) {
      return this._js13_default_get_onchange(values, kwargs)
    } else {
      return super._js_web2_onchange(ids, values, field_name, kwargs)
    }
  }

  async _js_web2_onchange(ids, values, field_name, kwargs = {}) {
    const version = this.env.server_version
    if (version > 13) {
      return super._js_web2_onchange(ids, values, field_name, kwargs)
    } else {
      return this._js13_web2_onchange(ids, values, field_name, kwargs)
    }
  }
}

class WebModelJS extends WebModel13 {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }
}

class WebModelServer extends WebModelJS {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }

  async _rpc_web2_name_search(kwargs = {}) {
    // const { name, args, operator, limit, offset } = kwargs
    const method = 'web2_name_search'
    const args = []
    return this.call_kw(method, args, kwargs)
  }

  _rpc_web2_fields_get_after(allfields, metadata) {
    const meta_get = fld => {
      const meta0 = metadata[fld]
      if (['many2one', 'many2many', 'one2many'].includes(meta0.type)) {
        if (Object.keys(meta0.metadata || {}).length === 0) {
          return meta0
        } else {
          const child_model = this.env.model(meta0.relation)
          const child_fields = (allfields[fld] || {}).fields || {}
          return {
            ...meta0,
            metadata: child_model._rpc_web2_fields_get_after(
              child_fields,
              meta0.metadata
            )
          }
        }
      } else {
        return meta0
      }
    }

    const fs2 = Object.keys(metadata).reduce((acc, fld) => {
      const meta1 = meta_get(fld)
      const meta2 = this.metadata_extend[fld] || {}
      const meta3 = {}
      const todo = allfields[fld] || {}
      if (todo.domain) {
        meta3.domain = todo.domain
      }
      if (todo.selection) {
        meta3.selection = todo.selection
      }
      acc[fld] = { ...meta1, ...meta2, ...meta3 }
      return acc
    }, {})

    return fs2
  }

  async _rpc_web2_fields_get_call(allfields, kwargs = {}) {
    const { attributes, context = {} } = kwargs
    const method = 'web2_fields_get'
    const args = [allfields, attributes]
    return this.call_kw(method, args, { context })
  }

  async _rpc_web2_fields_get(allfields, kwargs = {}) {
    const res = await this._rpc_web2_fields_get_call(allfields, kwargs)
    const metadata = this._rpc_web2_fields_get_after(allfields, res)
    this.metadata = { ...metadata }
    return this.metadata
  }

  async _rpc_web2_search_read(domain, specification, kwargs = {}) {
    const metadata = this.metadata
    const spec = this._js_format_fields_spec(specification, { metadata })
    const method = 'web2_search_read'
    const args = [domain, spec]
    return this.call_kw(method, args, kwargs)
  }

  async _rpc_web2_read(res_id, specification, kwargs = {}) {
    const metadata = this.metadata
    const spec = this._js_format_fields_spec(specification, { metadata })
    const method = 'web2_read'
    const args = [res_id, spec]
    return this.call_kw(method, args, kwargs)
  }

  async _rpc_web2_onchange(ids, values, field_name, kwargs = {}) {
    const method = 'web2_onchange'
    const metadata = this.metadata
    const fields_spec = this._js_fields_spec_for_onchange(metadata)

    const args = [ids, values, field_name, fields_spec]
    const res = await this.call_kw(method, args, kwargs)
    return res
  }

  async _rpc_web2_save(res_id, vals, kwargs = {}) {
    const method = 'web2_save'
    const metadata = this.metadata
    const specification = this._js_fields_spec_for_onchange(metadata)
    const args = [res_id, vals, specification]
    const res = await this.call_kw(method, args, kwargs)
    return res
  }
}

export class WebModel extends WebModelServer {
  constructor(model_name, payload = {}) {
    super(model_name, payload)
  }

  async web2_name_search(kwargs = {}) {
    // const { name, args, operator, limit, offset } = kwargs

    const run_by_server = this.env.run_by_server
    if (run_by_server) {
      return this._rpc_web2_name_search(kwargs)
    } else {
      return this._js_web2_name_search(kwargs)
    }
  }

  async web2_fields_get(fields, kwargs = {}) {
    // const { fields, context, callback } = kwargs
    // 参数 fields: dict. 参考 fields的定义
    //   参数 fields 中 包含 view 带过来的 额外的  domain 和 selection
    // 返回值. 对象类型. 包含每个字段的属性. 同 odoo的 fields_get函数
    // 若为 relation 字段. 则额外增加一个属性 metadata. 携带子模型的 metadata
    // 该函数有负作用. 将执行结果 metadata 在 模型中暂存

    function _clean_fields(fields) {
      return Object.keys(fields).reduce((acc, fld) => {
        const done = {}
        const todo = fields[fld] || {}
        if (todo.fields) {
          done.fields = _clean_fields(todo.fields)
        }

        if (todo.domain) {
          done.domain = todo.domain
        }

        if (todo.selection) {
          done.selection = todo.selection
        }

        acc[fld] = done
        return acc
      }, {})
    }

    const { context, callback } = kwargs
    const fields2 = _clean_fields(fields)

    const run_by_server = this.env.run_by_server
    if (run_by_server) {
      return this._rpc_web2_fields_get(fields2, { context })
    } else {
      return this._js_web2_fields_get(fields2, { callback, context })
    }
  }

  async web2_search_read(domain, specification, kwargs = {}) {
    // const { offset, limit, order, context, callback } = kwargs
    // 参数 offset, limit, order, context: 接口 web_search_read 需要的参数
    // 返回值. list. web_search_read 的返回值. 做标准化处理
    //
    const { callback, ...kwargs2 } = kwargs
    const run_by_server = this.env.run_by_server

    if (run_by_server) {
      return this._rpc_web2_search_read(domain, specification, kwargs2)
    } else {
      return this._js_web2_search_read(domain, specification, kwargs)
    }
  }

  async web2_read(res_id, specification, kwargs = {}) {
    // const { context, callback } = kwargs
    // 参数 res_ids 数据的 ids
    // 返回值. list. 同 web_read 的返回值. 做标准化处理
    //
    const { callback, ...kwargs2 } = kwargs
    const run_by_server = this.env.run_by_server

    if (run_by_server) {
      return this._rpc_web2_read(res_id, specification, kwargs2)
    } else {
      return this._js_web2_read(res_id, specification, kwargs)
    }
  }

  async web2_read_one(res_id, specification, kwargs = {}) {
    // const { context, callback } = kwargs
    // 参数 res_id 数据的 id
    // 参数 specification: Dict.  参考 fields的定义
    // 返回值. dict.

    const pick_one = data => (data.length > 0 ? data[0] : {})
    const { callback, ...kwargs2 } = kwargs
    const cb = callback ? data => callback(pick_one(data)) : undefined

    const res = await this.web2_read(res_id, specification, {
      callback: cb,
      ...kwargs2
    })

    return pick_one(res)
  }

  async web2_onchange(ids, values, field_name, kwargs = {}) {
    // 参数: ids, values, field_name
    // 关于 o2m 字段：
    // 自定义 扩展 模块 stock2.quant.picking.wizard 返回 line_ids,

    const run_by_server = this.env.run_by_server
    if (run_by_server) {
      return this._rpc_web2_onchange(ids, values, field_name, kwargs)
    } else {
      return this._js_web2_onchange(ids, values, field_name, kwargs)
    }
  }

  async web2_save(res_id, vals, kwargs = {}) {
    const run_by_server = this.env.run_by_server
    if (run_by_server) {
      return this._rpc_web2_save(res_id, vals, kwargs)
    } else {
      return this._js_web2_save(res_id, vals, kwargs)
    }
  }
}
